package ijen.fbconnect;


import ijen.fbconnect.helpers.FBParametersHelper;
import ijen.fbconnect.helpers.FBHelperFactory;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import java.util.HashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Pattern;


/**
 * Description of CallWorker.
 * ---------------------------------------------------------------------------------------
 * User: fred
 * Date: 22 juil. 2009
 * Time: 09:22:43
 * ---------------------------------------------------------------------------------------
 */
public final class FBRequest implements Runnable, FBResponseObserver {

    public enum Status  {
        UNKNOWN, LOADING, SUCCESS, FAILURE, ERROR
    }

    private static final Pattern         TOKEN_PATTERN ;
    private static final int MAX_REQUEST;
    private static final int MAX_BURST_TIME;
    private static final ExecutorService POOL;

    private static final Set<FBRequest>  QUEUE;

    static {
        TOKEN_PATTERN = Pattern.compile("^\"([0-9]|[a-f]){32}\"$");
        MAX_REQUEST = 3;
        MAX_BURST_TIME = 2;
        QUEUE         = new HashSet<FBRequest>();
        POOL          = Executors.newFixedThreadPool(MAX_REQUEST);
    }

    private long lastCall;

    private Map<String,String> parameters;
    private FBResponseObserver observer;
    private FBParametersHelper parametersHelper;

    private FBSession session;
    private String    methodName;
    private Status    status;
    private Boolean   needsMultipartEncoding;

    public static final String FACEBOOK_RESTSERVER_URL        = "http://api.facebook.com/restserver.php";
    public static final String FACEBOOK_RESTSERVER_SECURE_URL = "https://api.facebook.com/restserver.php";

    private static final String USER_AGENT = "FacebookConnect";


    /**
     * <p>Create a new FB request, with method name set. Uses default singleton instance of FBSession.
     * <p>User will have to pass parameters on later call.
     * @param pMethodName Method name.
     */
    public FBRequest( final String pMethodName) {
        assert pMethodName != null;

        methodName = pMethodName;
        parameters = new HashMap<String, String>();
        session = FBSession.getInstance();
        parametersHelper = FBHelperFactory.getFBParametersHelper();
        status = Status.UNKNOWN;
        needsMultipartEncoding = false;     // By default;
    }


    public FBRequest(final FBSession pSession, final String pMethodName) {

        this(pMethodName);

        assert pSession != null;

        lastCall = 0L;
        session = pSession;
    }


    public FBRequest(final FBSession pSession, final String pMethodName, final FBResponseObserver pObserver) {
        this(pSession, pMethodName);

        assert pObserver != null;

        observer   = pObserver;
    }


    public FBRequest(final FBResponseObserver pObserver) {
        assert pObserver != null;

        observer = pObserver;
        session = FBSession.getInstance();
    }


    /**
     * <p>Execution point.
     */
    public void run() {
        doCall();
    }


    /**
     * <p>Callback for success.
     * @param response HTTP response string
     */
    public void success(final String response) {
        status = Status.SUCCESS;
        dequeue();

        if (observer != null) {
            observer.success(response);
        }
    }


    /**
     * <p>Callback for failures.
     * @param response HTTP response string
     */
    public void failure(final String response) {
        status = Status.FAILURE;
        dequeue();

        if (observer != null) {
            observer.failure(response);
        }
    }


    /**
     * <p>Callback for errors.
     * @param response  HTTP response string
     */
    public void error(final String response) {
        status = Status.ERROR;
        dequeue();

        if (observer != null) {
            observer.error(response);
        }
    }


    /**
     * <p>The meaty part. Format requests and call asynchronously.
     */
    private void doCall() {
        assert parameters != null;
        assert parametersHelper != null;

        String result;
        HttpResponse response;

        HttpClient client = new DefaultHttpClient();
        HttpPost post = new HttpPost(FACEBOOK_RESTSERVER_URL);
        post.setHeader("User-Agent", USER_AGENT);

        try {   // Create an entity an attach it to post method, and check if encoding is needed

            post.setEntity(
                    needsMultipartEncoding ?
                    parametersHelper.formatMultipartParameters(parameters) :        
                    new UrlEncodedFormEntity(parametersHelper.getNameValuePairsFrom(parameters), HTTP.UTF_8));

        } catch (UnsupportedEncodingException e) {
            error("Could not encode parameters");
            return;
        }

        try {
            lastCall = System.currentTimeMillis();  // Set last call for burst calculation
            status   = Status.LOADING;              // Change status to loading
            response = client.execute(post);        // Execute API call
        } catch (IOException e) {
            error("Failed to call Facebook API");
            return;
        }

        assert response != null;

        try {   // Format response as String
            result = EntityUtils.toString(response.getEntity());
        } catch (IOException e) {
            error("Could not get or format response to string");
            return;
        }

        //System.out.println(result);

        JSONObject json;

        try {
            json = new JSONObject(result);
        } catch (JSONException e) {

            try {   // Could be an array result, errors are JSONObject type
                new JSONArray(result);  // Avoid useless var (findbugs)
                success(result);        // If we successfully parsed an array, it's a valid result
                return;
            } catch (JSONException e1) {

                if (//parametersHelper.isSpecialMethod(parameters) &&
                        TOKEN_PATTERN.matcher(result).matches()) {
                    /*
                     * FBSession sometimes return invalid JSON, f.i. for auth.createToken API call
                     * However they must match the following pattern to be considered valid
                     */
                    success(result.replaceAll("\"", ""));
                } else {
                    error("Could not parse JSON result (neither a JSONObjet nor a JSONArray)");
                }

                return;
            }
        }

        try {
            String error_code = json.getString("error_code");
            String error_msg  = json.getString("error_msg");
            failure(String.format("API returned error ('error code:%s - %s')", error_code, error_msg));     // If we were able to parse error code, failure

        } catch (JSONException e) {
            success(result);
        }
    }


    /**
     * <p>Do call to Facebook REST API.
     * <p>This method automatically enqueue requests and check burst
     * @param pSession A Valid FBsession object
     * @param pMethodName Name of the method to call
     * @param pParameters List of parameters for request
     */
    private void call(final FBSession pSession, final String pMethodName, final Map<String,String> pParameters) {
        assert QUEUE != null;
        assert parametersHelper != null;
        assert POOL != null;

        parametersHelper.updateAndSignParameters(pSession, pMethodName, pParameters);
        setParameters(pParameters);

        if (enqueue()) {
            POOL.execute(this);
        } else {
            // Should notify...
            System.out.println("Could not enqueue " + this);
        }
    }


    /**
     *
     * <p>Convenience method for FBSession.send().
     */
    void call() {
        call(session, methodName, parameters);
    }


    /**
     * <p>Main entrance point for FBRequest and making Facebook API calls.
     * <p>Use the shared FBSession instance to mahke calls (ensure it's correctly configured).
     * <p>All the signing and tedious work on parameters is handled.
     * @param pMethodName Name of the API REST method
     * @param pParameters List of parameters
     */
    public void call(final String pMethodName, final Map<String,String> pParameters) {
        call(FBSession.getInstance(), pMethodName, pParameters);
    }


    /**
     * <p>Enqueue a request, checks that does not exceed maximum limit, and that burst time is ok
     * @return True if correctly enqueued.
     */
    public boolean enqueue() {
        assert QUEUE != null;

        long t = lastCall == 0 ? 0 : (System.currentTimeMillis() - lastCall);

        if (QUEUE.size() < MAX_REQUEST
                && t < (MAX_BURST_TIME * 1000) ) {
            QUEUE.add(this);
        } else {
            System.out.printf("Can't enqueue: request:%s, size:%d, last_call:%s ms\n", this, QUEUE.size() ,t);
        }

        return QUEUE.contains(this);
    }


    /**
     * <p>Dequeue a request after completion (success,error or failure).
     * @return True if correctly dequeued.
     */
    public boolean dequeue() {
        assert QUEUE != null;

        if (!QUEUE.isEmpty()) {
            QUEUE.remove(this);
        }

        return !QUEUE.contains(this);
    }

    
    /**
     * <p>Specify that parameters must be multipart encoded be sending request.
     * <p>Most probably because of photo upload.
     */
    public void needsMultipartEncoding() {
        needsMultipartEncoding = true;
    }


    /**
     * <p>Resets to default encoding behaviour (no multipart/form-data)
     * <p>Might be slightly non-standard towards Facebook API call / FBConnect
     * , but send a lot less data over network and works !
     */
    public void needsDefaultEncoding() {
        needsMultipartEncoding = false;
    }


    /* -- Getters / setters -- */

    public void setParameters(final Map<String, String> pParameters) {
        parameters = pParameters;
    }

    public Map<String, String> getParameters() {
        return parameters;
    }

    public FBResponseObserver getObserver() {
        return observer;
    }

    public void setObserver(FBResponseObserver pObserver) {
        observer = pObserver;
    }

    public String getMethodName() {
        return methodName;
    }

    public void setMethodName(String methodName) {
        this.methodName = methodName;
    }

    public Status getStatus() {
        return status;
    }


}